﻿//-----------------------------------------------------------------------
// <copyright file="MembershipRepository.cs" company="OOHM PROCESSAMENTO DE DADOS LTDA.">
//     Microsoft Public License (MS-PL) This license governs use of the accompanying
//     software. If you use the software, you accept this license. If you do not
//     accept the license, do not use the software. 1. Definitions The terms "reproduce,"
//     "reproduction," "derivative works," and "distribution" have the same meaning
//     here as under U.S. copyright law. A "contribution" is the original software,
//     or any additions or changes to the software. A "contributor" is any person
//     that distributes its contribution under this license. "Licensed patents"
//     are a contributor's patent claims that read directly on its contribution.
//     2. Grant of Rights (A) Copyright Grant- Subject to the terms of this license,
//     including the license conditions and limitations in section 3, each contributor
//     grants you a non-exclusive, worldwide, royalty-free copyright license to
//     reproduce its contribution, prepare derivative works of its contribution,
//     and distribute its contribution or any derivative works that you create.
//     (B) Patent Grant- Subject to the terms of this license, including the license
//     conditions and limitations in section 3, each contributor grants you a non-exclusive,
//     worldwide, royalty-free license under its licensed patents to make, have
//     made, use, sell, offer for sale, import, and/or otherwise dispose of its
//     contribution in the software or derivative works of the contribution in
//     the software. 3. Conditions and Limitations (A) No Trademark License- This
//     license does not grant you rights to use any contributors' name, logo, or
//     trademarks. (B) If you bring a patent claim against any contributor over
//     patents that you claim are infringed by the software, your patent license
//     from such contributor to the software ends automatically. (C) If you distribute
//     any portion of the software, you must retain all copyright, patent, trademark,
//     and attribution notices that are present in the software. (D) If you distribute
//     any portion of the software in source code form, you may do so only under
//     this license by including a complete copy of this license with your distribution.
//     If you distribute any portion of the software in compiled or object code
//     form, you may only do so under a license that complies with this license.
//     (E) The software is licensed "as-is." You bear the risk of using it. The
//     contributors give no express warranties, guarantees or conditions. You may
//     have additional consumer rights under your local laws which this license
//     cannot change. To the extent permitted under your local laws, the contributors
//     exclude the implied warranties of merchantability, fitness for a particular
// </copyright>
//-----------------------------------------------------------------------
namespace AbstractSaaSTemplate.Infrastructure.Win2K8WithSQLServer.Repositories{    using System;
    using System.Collections.Generic;
    using System.Globalization;
    using System.Linq;
    using AbstractSaaSTemplate.Domain.Models.Entities;
    using AbstractSaaSTemplate.Domain.Models.Enumerators;
    using AbstractSaaSTemplate.Domain.Models.ValueObjects;
    using AbstractSaaSTemplate.Domain.Repositories;
    using OOHM.Framework.Security.Cryptography;
    
    /// <summary>
    /// Implements 'IMembershipRepository' and all its contract, providing
    /// data accessibility to the aggregate 'Membership'.
    /// </summary>
    public sealed class MembershipRepository : SQLServerConnectedRepositoryBase, IMembershipRepository
    {
        /// <summary>
        /// Initializes a new instance of the MembershipRepository class.
        /// </summary>
        /// <param name="loaderForConnectionString">The delegate capable of retrieving the connection string for the EF4 context.</param>
        public MembershipRepository(Func<string> loaderForConnectionString)
            : base(loaderForConnectionString)
        {
        }

        /// <summary>
        /// Retrieves an account by its id.
        /// </summary>
        /// <param name="id">The id of the account.</param>
        /// <returns>If found, returns an instance of Account; otherwise returns null.</returns>
        public Account GetAccountById(long id)
        {
            using (var context = NewContext())
            {
                var account = context.Accounts
                    .Where(d => d.Id == id)
                    .FirstOrDefault();

                if (account == null)
                {
                    return null;
                }

                var entity = new Account();
                entity.Id = account.Id;

                CopyFromEF4ToDomain<Core.Account, Account>(
                    account,
                    entity);

                return entity;
            }
        }

        /// <summary>
        /// Creates or updates a 'User' in the system.
        /// </summary>
        /// <param name="entity">The entry of 'User' which will be either created or updated.</param>
        public void AddOrUpdateUser(User entity)
        {
            if (entity == null)
            {
                throw new ArgumentNullException("entity");
            }

            using (var context = NewContext())
            {
                // we attempt to retrieve the user
                // from SQL by using the EF4 Model
                var user = context.Users
                    .Where(d => d.Id == entity.Id)
                    .FirstOrDefault();

                // if there's no user matching the entity's Id
                // we create a new user and add it to the context
                if (user == null)
                {
                    user = new Core.User();
                    context.Users.AddObject(user);
                }

                // this static helper copy users with
                // the same name. beware of unmatching types
                // for properties with the same name
                CopyFromDomainToEF4<Domain.Models.Entities.User, Core.User>(
                    entity,
                    user);

                // not all items are saved in the way that
                // they are kept in the domain. for those cases...
                user.CultureName = entity.Culture.Name;
                user.AccessLevel = entity.AccessLevel.ToString();

                // after loading the data into the EF model entity,
                // we run all inserts, updates and delete commands
                // by invoking the SubmitChanges();
                context.SaveChanges();

                // also, credentials are managed by another table
                // in our EF4 model. We should check new > save it there
                var password = entity.Credentials.Password.ToArray();
                var credentials = context.Credentials
                    .Where(d =>
                        d.UserId.HasValue &&
                        d.UserId == user.Id &&
                        d.Login == entity.Credentials.Login &&
                        d.Password == password)
                    .FirstOrDefault();

                // if the credentials were not generated or do not match
                // we revoke all previous credentials which have not been
                // revoked and created a new one
                if (credentials == null)
                {
                    // here we get all credentials which have not yet
                    // been revoked and revoke them all
                    var credentialsToRevoke = context.Credentials
                        .Where(d =>
                            d.UserId.HasValue &&
                            d.UserId == user.Id &&
                            !d.RevokedAt.HasValue);

                    foreach (var credentialToRevoke in credentialsToRevoke)
                    {
                        credentialToRevoke.RevokedAt = DateTime.Now;
                    }

                    // and finally we create a new entry of
                    // credential and save it.
                    credentials = new Core.Credential()
                    {
                        Id = Guid.NewGuid(),
                        UserId = user.Id,
                        Login = entity.Credentials.Login,
                        Password = entity.Credentials.Password.ToArray(),
                        CreatedAt = DateTime.Now
                    };

                    context.Credentials.AddObject(credentials);

                    // after all that, you save changes
                    context.SaveChanges();
                }

                // in case there is an IDENTITY column with ID generated
                // by the DB, we load it back into the Domain entity.
                entity.Id = user.Id;
            }
        }

        /// <summary>
        /// Retrieves a tenant by its website url.
        /// </summary>
        /// <param name="websiteUrl">The absolute uri of the website of the tenant.</param>
        /// <returns>Returns an instance of tenant, if found; otherwise, returns null.</returns>
        public Tenant GetTenantByWebsite(Uri websiteUrl)
        {
            if (websiteUrl == null)
            {
                throw new ArgumentNullException("websiteUrl");
            }

            using (var context = NewContext())
            {
                var tenant = context.Tenants
                    .Where(d => d.Website == websiteUrl.AbsoluteUri)
                    .FirstOrDefault();

                // otherwise we attempt to copy all data from
                // the EF4 model entity into our domain model
                var entity = new Tenant();
                entity.Id = tenant.Id;
                CopyFromEF4ToDomain<Core.Tenant, Domain.Models.Entities.Tenant>(
                    tenant,
                    entity);

                // we load data which can't be copied from the EF4 entity
                // and we return the domain entity
                return entity;
            }
        }

        /// <summary>
        /// Gets a 'User' by its id.
        /// </summary>
        /// <param name="id">The id of the User.</param>
        /// <returns>Returns an instance of User.</returns>
        public User GetUserById(long id)
        {
            using (var context = NewContext())
            {
                var user = context.Users
                    .Where(d => d.Id == id)
                    .FirstOrDefault();

                if (user == null)
                {
                    return null;
                }

                var credential = user.Credentials
                    .Where(d => !d.RevokedAt.HasValue)
                    .OrderByDescending(d => d.CreatedAt)
                    .FirstOrDefault();

                var entity = TransformDbUserIntoDomainUser(
                    user,
                    credential);

                return entity;
            }
        }
        
        /// <summary>
        /// Gets a 'User' by its login.
        /// </summary>
        /// <param name="login">The login which will be matched to find the user.</param>
        /// <returns>If found, returns the instance of 'User'; otherwise, returns null.</returns>
        public User GetUserByCredentials(string login)
        {
            return this.GetUserByCredentials(login, null);
        }

        /// <summary>
        /// Gets a 'User' by its login and password.
        /// </summary>
        /// <param name="login">The login which will be matched to find the user.</param>
        /// <param name="password">The password which will be mached to find the user.</param>
        /// <returns>If found, returns the instance of 'User'; otherwise, returns null.</returns>
        public User GetUserByCredentials(string login, string password)
        {
            var encryptedPassword = null as byte[];
            if (!string.IsNullOrWhiteSpace(password))
            {
                encryptedPassword = Cypher.Encrypt(password);
            }
            
            using (var context = NewContext())
            {
                var credential = context.Credentials
                    .Where(d =>
                        d.Login == login &&
                        (string.IsNullOrEmpty(password) || d.Password == encryptedPassword))
                    .FirstOrDefault();

                if (credential == null)
                {
                    return null;
                }

                if (!credential.UserId.HasValue)
                {
                    return null;
                }
                
                // we attempt to retrieve the user
                // from SQL related to those Credentials
                var user = context.Users
                    .Where(d => d.Id == credential.UserId.Value)
                    .FirstOrDefault();

                return TransformDbUserIntoDomainUser(
                    user,
                    credential);
            }
        }

        /// <summary>
        /// Gets a 'User' by its email address.
        /// </summary>
        /// <param name="emailAddress">The emailAddress which will be matched to find the user.</param>
        /// <returns>If found, returns the instance of 'User'; otherwise, returns null.</returns>
        public User GetUserByEmailAddress(string emailAddress)
        {
            using (var context = NewContext())
            {
                // we attempt to retrieve the user
                // from SQL by using the EF4 Model
                var user = context.Users
                    .Where(d => d.EmailAddress == emailAddress)
                    .FirstOrDefault();

                var credential = null as Core.Credential;
                if (user != null)
                {
                    credential = context.Credentials
                        .Where(d =>
                            d.UserId == user.Id &&
                            !d.RevokedAt.HasValue)
                        .OrderByDescending(d => d.CreatedAt)
                        .FirstOrDefault();
                }

                return TransformDbUserIntoDomainUser(
                    user,
                    credential);
            }
        }

        /// <summary>
        /// Retrieves a list of 'User' who match to the role
        /// </summary>
        /// <param name="role">Returns a list of User.</param>
        /// <returns>Returns a list of User</returns>
        public IEnumerable<User> GetUsersByRole(AccessRoles role)
        {
            using (var context = NewContext())
            {
                var databaseUsers = context.UserRoles
                    .Where(d => d.Role == role.ToString())
                    .Join(
                        context.Users,
                        userRoles => userRoles.UserId,
                        users => users.Id,
                        (userRoles, users) => users)
                    .ToArray();

                var entities = new List<User>(databaseUsers.Length);
                foreach (var databaseUser in databaseUsers)
                {
                    var databaseCredential = databaseUser.Credentials
                        .Where(d => !d.RevokedAt.HasValue)
                        .OrderByDescending(d => d.CreatedAt)
                        .FirstOrDefault();
                    
                    var entity = TransformDbUserIntoDomainUser(
                        databaseUser,
                        databaseCredential);

                    entities.Add(entity);
                }

                return entities;
            }
        }

        /// <summary>
        /// Retrieves a list of 'User' who match to the role
        /// </summary>
        /// <param name="level">The accesslevel for which we will try to find a matching user.</param>
        /// <param name="role">The role for which we will try to find a matching user.</param>
        /// <returns>Returns a list of User</returns>
        public IEnumerable<User> GetUsersByRole(AccessLevels level, AccessRoles role)
        {
            using (var context = NewContext())
            {
                var parsedLevel = level.ToString();
                var parsedRole = role.ToString();

                var databaseUsers = context.UserRoles
                    .Where(d => d.Role == parsedRole)
                    .Join(
                        context.Users,
                        userRoles => userRoles.UserId,
                        users => users.Id,
                        (userRoles, users) => users)
                    .Where(d => d.AccessLevel == parsedLevel)
                    .ToArray();

                var entities = new List<User>(databaseUsers.Length);
                foreach (var databaseUser in databaseUsers)
                {
                    var databaseCredential = databaseUser.Credentials
                        .Where(d => !d.RevokedAt.HasValue)
                        .OrderByDescending(d => d.CreatedAt)
                        .FirstOrDefault();

                    var entity = TransformDbUserIntoDomainUser(
                        databaseUser,
                        databaseCredential);

                    entities.Add(entity);
                }

                return entities;
            }
        }

        /// <summary>
        /// Retrieves a list of 'User' who are related to a tenant.
        /// </summary>
        /// <param name="tenantId">The id of the tenant.</param>
        /// <returns>Returns a list of User</returns>
        public IEnumerable<User> GetUsersByTenantId(long tenantId)
        {
            using (var context = NewContext())
            {
                var databaseUsers = context.UserTenants
                    .Where(d => d.TenantId == tenantId)
                    .Join(
                        context.Users,
                        userTenants => userTenants.UserId,
                        users => users.Id,
                        (userRoles, users) => users)
                    .ToArray();

                var entities = new List<User>(databaseUsers.Length);
                foreach (var databaseUser in databaseUsers)
                {
                    var databaseCredential = databaseUser.Credentials
                        .Where(d => !d.RevokedAt.HasValue)
                        .OrderByDescending(d => d.CreatedAt)
                        .FirstOrDefault();

                    var entity = TransformDbUserIntoDomainUser(
                        databaseUser,
                        databaseCredential);

                    entities.Add(entity);
                }

                return entities;
            }
        }

        /// <summary>
        /// Retrieves a list of 'User' who are related to a tenant.
        /// </summary>
        /// <param name="tenantId">The id of the tenant.</param>
        /// <param name="role">The role for which we will retrieve the users</param>
        /// <returns>Returns a list of User</returns>
        public IEnumerable<User> GetUsersByTenantIdAndRole(long tenantId, AccessRoles role)
        {
            using (var context = NewContext())
            {
                var parsedRole = role.ToString();
                var databaseUsers = context.UserTenants
                    .Where(d => d.TenantId == tenantId)
                    .Join(
                        context.UserRoles,
                        userTenants => userTenants.UserId,
                        userRoles => userRoles.UserId,
                        (userTenants, userRoles) => userRoles)
                    .Where(d => d.Role == parsedRole)
                    .Join(
                        context.Users,
                        userRoles => userRoles.UserId,
                        users => users.Id,
                        (userRoles, users) => users)
                    .ToArray();

                var entities = new List<User>(databaseUsers.Length);
                foreach (var databaseUser in databaseUsers)
                {
                    var databaseCredential = databaseUser.Credentials
                        .Where(d => !d.RevokedAt.HasValue)
                        .OrderByDescending(d => d.CreatedAt)
                        .FirstOrDefault();

                    var entity = TransformDbUserIntoDomainUser(
                        databaseUser,
                        databaseCredential);

                    entities.Add(entity);
                }

                return entities;
            }
        }

        /// <summary>
        /// Gets the list of roles related to a particular user.
        /// </summary>
        /// <param name="login">The login of the user for which the roles will be loaded.</param>
        /// <returns>Returns a list of Role</returns>
        public IEnumerable<AccessRoles> GetRolesByCredentials(string login)
        {
            using (var context = NewContext())
            {
                // prepares the default returning list.
                var entities = new List<AccessRoles>();

                // find the creential via login
                var credential = context.Credentials
                    .Where(d => d.Login == login)
                    .FirstOrDefault();

                if (credential == null)
                {
                    return entities;
                }

                if (!credential.UserId.HasValue)
                {
                    return entities;
                }

                // we attempt to retrieve the roles for the user
                // from SQL by using the EF4 Model. reuser, that
                // SQL allows you to use InnerJoins (thank god)
                var roleNames = Enum.GetNames(typeof(AccessRoles));
                foreach (var roleName in roleNames)
                {
                    var role = AccessRoles.None;
                    if (!Enum.TryParse<AccessRoles>(roleName, out role))
                    {
                        continue;
                    }

                    if (role == AccessRoles.None)
                    {
                        continue;
                    }

                    if (!entities.Contains(role))
                    {
                        entities.Add(role);
                    }
                }

                // if no roles is found for the user, we are unable to
                // retrieve any domain user as well. We therefore create
                // an empty array which prevent unnecessary NullReferenceExceptions
                if (roleNames == null || roleNames.Length == 0)
                {
                    return entities;
                }

                // otherwise we attempt to copy all data from
                // the EF4 model entity into our domain model
                foreach (var databaseRole in roleNames)
                {
                    var role = AccessRoles.None;

                    if (!Enum.TryParse<AccessRoles>(databaseRole, out role))
                    {
                        continue;
                    }

                    if (role == AccessRoles.None)
                    {
                        continue;
                    }

                    entities.Add(role);
                }

                return entities;
            }
        }
              
        /// <summary>
        /// Generates an association between the user
        /// and the key of the role.
        /// </summary>
        /// <param name="userId">The id of the user.</param>
        /// <param name="role">The key of the role.</param>
        /// <returns>Returns a instance of 'UserRole' which is the association.</returns>
        public UserRole AssociateUserWithRole(long userId, AccessRoles role)
        {
            using (var context = NewContext())
            {
                var parsedRole = role.ToString();
                var userRole = context.UserRoles
                    .Where(d =>
                        d.UserId == userId &&
                        d.Role == parsedRole)
                    .FirstOrDefault();

                if (userRole == null)
                {
                    userRole = new Core.UserRole();
                    userRole.UserId = userId;
                    userRole.Role = role.ToString();
                    userRole.CreatedAt = DateTime.Now;

                    context.UserRoles.AddObject(userRole);
                    context.SaveChanges();
                }

                var entity = new UserRole();
                entity.UserId = userRole.UserId;
                entity.Role = role;
                entity.CreatedAt = userRole.CreatedAt;

                return entity; 
            }
        }

        /// <summary>
        /// Generates an association between the user and the tenant.
        /// </summary>
        /// <param name="userId">The id of the user.</param>
        /// <param name="tenantId">The id of the tenant.</param>
        /// <returns>Returns a instance of 'UserTenant' which is the association.</returns>
        public UserTenant AssociateUserWithTenant(long userId, long tenantId)
        {
            using (var context = NewContext())
            {
                var userTenant = context.UserTenants
                    .Where(d =>
                        d.UserId == userId &&
                        d.TenantId == tenantId)
                    .FirstOrDefault();

                if (userTenant == null)
                {
                    userTenant = new Core.UserTenant();
                    userTenant.UserId = userId;
                    userTenant.TenantId = tenantId;
                    userTenant.CreatedAt = DateTime.Now;

                    context.UserTenants.AddObject(userTenant);
                    context.SaveChanges();
                }

                var entity = new UserTenant();
                entity.UserId = userTenant.UserId;
                entity.TenantId = userTenant.TenantId;
                entity.CreatedAt = userTenant.CreatedAt;

                return entity;                
            }
        }

        /// <summary>
        /// Retrieves a user x role association if existing.
        /// </summary>
        /// <param name="user">The user for which we are attempting to retrieve the association.</param>
        /// <param name="role">The role for which we are attempting to retrieve the association.</param>
        /// <returns>If exists, returns a UserRole association; otherwise returns false.</returns>
        public UserRole GetUserRoleAssociation(User user, AccessRoles role)
        {
            if (user == null)
            {
                throw new ArgumentNullException("user");
            }

            using (var context = NewContext())
            {
                var parsedRole = role.ToString();
                var userRole = context.UserRoles
                    .Where(d =>
                        d.UserId == user.Id &&
                        d.Role == parsedRole)
                    .FirstOrDefault();

                if (userRole == null)
                {
                    return null;
                }

                var entity = new UserRole();
                entity.UserId = user.Id;
                entity.Role = role;
                return entity;
            }
        }

        /// <summary>
        /// Generates an association between the role and the controlled feature.
        /// </summary>
        /// <param name="level">The access level of the role and feature.</param>
        /// <param name="role">The role which will be checked.</param>
        /// <param name="feature">The feature which is being accessed.</param>
        /// <returns>Returns a instance of 'RoleFeature' which is the association.</returns>
        public RoleFeature AssociateRoleWithFeature(
            AccessLevels level,
            AccessRoles role,
            ControlledFeatures feature)
        {
            using (var context = NewContext())
            {
                var parsedLevel = level.ToString();
                var parsedRole = role.ToString();
                var parsedFeature = feature.ToString();
                
                var roleFeature = context.RoleFeatures
                    .Where(d =>
                        d.Level == parsedLevel &&
                        d.Role == parsedRole &&
                        d.Feature == parsedFeature)
                    .FirstOrDefault();

                if (roleFeature == null)
                {
                    roleFeature = new Core.RoleFeature();
                    roleFeature.Level = parsedLevel;
                    roleFeature.Role = parsedRole;
                    roleFeature.Feature = parsedFeature;
                    roleFeature.CreatedAt = DateTime.Now;

                    context.RoleFeatures.AddObject(roleFeature);
                    context.SaveChanges();
                }

                var entity = new RoleFeature();
                entity.Level = level;
                entity.Role = role;
                entity.Feature = feature;
                entity.CreatedAt = roleFeature.CreatedAt;

                return entity;
            }
        }

        /// <summary>
        /// Removes an association related to level, role and feature.
        /// </summary>
        /// <param name="level">The access level of the role and feature.</param>
        /// <param name="role">The role which will be checked.</param>
        /// <param name="feature">The feature which is being accessed.</param>
        public void RemoveAssociationBetweenRoleAndFeature(
            AccessLevels level,
            AccessRoles role,
            ControlledFeatures feature)
        {
            using (var context = NewContext())
            {
                var parsedLevel = level.ToString();
                var parsedRole = role.ToString();
                var parsedFeature = feature.ToString();
                
                var roleFeature = context.RoleFeatures
                    .Where(d =>
                        d.Level == parsedLevel &&
                        d.Role == parsedRole &&
                        d.Feature == parsedFeature)
                    .FirstOrDefault();

                if (roleFeature != null)
                {
                    context.RoleFeatures.DeleteObject(roleFeature);
                }

                context.SaveChanges();
            }
        }

        /// <summary>
        /// Retrieves a AccessLevel and Role x Feature association if existing.
        /// </summary>
        /// <param name="level">The access level of the role and feature.</param>
        /// <param name="role">The role which will be checked.</param>
        /// <param name="feature">The feature which is being accessed.</param>
        /// <returns>If exists, returns a RoleFeature association; otherwise returns false.</returns>
        public RoleFeature GetRoleFeatureAssociation(
            AccessLevels level,
            AccessRoles role,
            ControlledFeatures feature)
        {
            using (var context = NewContext())
            {
                var parsedLevel = level.ToString();
                var parsedRole = role.ToString();
                var parsedFeature = feature.ToString();

                var roleFeature = context.RoleFeatures
                    .Where(d =>
                        d.Level == parsedLevel &&
                        d.Role == parsedRole &&
                        d.Feature == parsedFeature)
                    .FirstOrDefault();

                if (roleFeature == null)
                {
                    return null;
                }

                var entity = new RoleFeature();
                entity.Level = level;
                entity.Role = role;
                entity.Feature = feature;

                return entity;
            }
        }

        /// <summary>
        /// Gets a list of all features related to the role for a specific access level.
        /// </summary>
        /// <param name="level">The access level.</param>
        /// <returns>Returns a list of associations between role, access levels and features.</returns>
        public IEnumerable<RoleFeature> GetRoleFeaturesByAccessLevel(AccessLevels level)
        {
            using (var context = NewContext())
            {
                var parsedLevel = level.ToString();
                
                var roleFeatures = context.RoleFeatures
                    .Where(d => d.Level == parsedLevel)
                    .ToArray();

                var entities = new List<RoleFeature>(roleFeatures.Length);
                foreach (var roleFeature in roleFeatures)
                {
                    var auxLevel = AccessLevels.None;
                    if (!Enum.TryParse<AccessLevels>(roleFeature.Level, out auxLevel))
                    {
                        continue;
                    }
                    
                    var auxRole = AccessRoles.None;
                    if (!Enum.TryParse<AccessRoles>(roleFeature.Role, out auxRole))
                    {
                        continue;
                    }

                    var auxFeature = ControlledFeatures.None;
                    if (!Enum.TryParse<ControlledFeatures>(roleFeature.Feature, out auxFeature))
                    {
                        continue;
                    }

                    var entity = new RoleFeature();
                    entity.Level = auxLevel;
                    entity.Role = auxRole;
                    entity.Feature = auxFeature;

                    entities.Add(entity);
                }

                return entities;
            }
        }

        /// <summary>
        /// Retrives the list of User related to a particular access level.
        /// </summary>
        /// <param name="level">The access level for which the Users will be retrieved.</param>
        /// <returns>Returns an enumerable of User related to a particular access level.</returns>
        public IEnumerable<User> GetUsers(AccessLevels level)
        {
            return this.GetUsers(
                default(long),
                level);
        }

        /// <summary>
        /// Retrives the list of User related to a particular access level.
        /// </summary>
        /// <param name="visibleToUserId">The id of the user, so that we can check for related tenants or accounts, depending on the level.</param>
        /// <param name="level">The access level for which the Users will be retrieved.</param>
        /// <returns>Returns an enumerable of User related to a particular access level.</returns>
        public IEnumerable<User> GetUsers(long visibleToUserId, AccessLevels level)
        {
            using (var context = NewContext())
            {
                var parsedLevel = level.ToString();
                
                var databaseUser = null as IList<Core.User>;
                if (level == AccessLevels.Distributor)
                {
                    databaseUser = context.Users
                        .Where(d => d.AccessLevel == parsedLevel)
                        .ToArray();
                }
                else if (level == AccessLevels.Tenant)
                {
                    var tenantIds = context.UserTenants
                        .Where(d => d.UserId == visibleToUserId)
                        .Select(d => d.TenantId)
                        .ToArray();

                    databaseUser = context.UserTenants
                        .Where(d => tenantIds.Contains(d.TenantId))
                        .Join(
                            context.Users,
                            userTenants => userTenants.UserId,
                            users => users.Id,
                            (userTenants, users) => users)
                        .ToArray();
                }
                else
                {
                    var accountIds = context.UserAccounts
                        .Where(d => d.UserId == visibleToUserId)
                        .Select(d => d.AccountId)
                        .ToArray();

                    databaseUser = context.UserAccounts
                        .Where(d => accountIds.Contains(d.AccountId))
                        .Join(
                            context.Users,
                            userAccounts => userAccounts.UserId,
                            users => users.Id,
                            (userAccounts, users) => users)
                        .ToArray();
                }

                var entities = new List<User>(databaseUser.Count);
                foreach (var user in databaseUser)
                {
                    var databaseCredential = user.Credentials
                        .Where(d => !d.RevokedAt.HasValue)
                        .OrderByDescending(d => d.CreatedAt)
                        .FirstOrDefault();

                    var entity = TransformDbUserIntoDomainUser(user, databaseCredential);
                    entities.Add(entity);
                }

                return entities;
            }
        }

        /// <summary>
        /// Retrieves users from the DB users, copying all data
        /// from one into the other.
        /// </summary>
        /// <param name="databaseUser">The EF4 User</param>
        /// <param name="databaseCredential">The EF4 Credential</param>
        /// <returns>Returns an instance o User (if the dbUser is not null); otherwise returns null.</returns>
        private static User TransformDbUserIntoDomainUser(
            Core.User databaseUser,
            Core.Credential databaseCredential)
        {
            // if no user is found, we are unable
            // to retrieve any domain user as well
            if (databaseUser == null)
            {
                return null;
            }

            if (databaseCredential == null)
            {
                throw new ArgumentNullException("databaseCredential");
            }

            // otherwise we attempt to copy all data from
            // the EF4 model entity into our domain model
            var entity = new User();
            CopyFromEF4ToDomain<Core.User, Domain.Models.Entities.User>(
                databaseUser,
                entity);

            // password in the DB is encrypted. we have to decrypt it
            var decryptedPassword = Cypher.Decrypt(databaseCredential.Password);
            
            // we load data which can't be copied from the EF4 entity
            entity.Id = databaseUser.Id;
            entity.Culture = new CultureInfo(databaseUser.CultureName);
            entity.Credentials = new Credentials(
                databaseCredential.Login,
                decryptedPassword);

            var auxLevel = AccessLevels.None;
            if (Enum.TryParse<AccessLevels>(databaseUser.AccessLevel, out auxLevel))
            {
                entity.AccessLevel = auxLevel;
            }

            return entity;
        }
    }
}


